import collections
import numpy 
from copy import deepcopy
from re   import search as re_search
from .functions import RTOL, ATOL, equals

# ====================================================================
#
# CfList object
#
# ====================================================================

class CfList(collections.MutableSequence):
    '''

A list-like object with attributes.

'''
    def __init__(self, sequence=()):
        '''

**Initialization**

:Parameters:

    sequence : iterable, optional
         Define a new list with these elements.

'''
        self.__dict__['_list'] = []

        try:
            self.__dict__['_list'].extend(sequence)
        except TypeError:
            self.__dict__['_list'].append(sequence)
    #--- End: def

    def __deepcopy__(self, memo):
        '''
Used if copy.deepcopy is called on the variable.

''' 
        return self.copy()
    #--- End: def

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)

'''
        return repr(self._list)
    #--- End: def

    def __str__(self):
        '''
x.__str__() <==> str(x)

'''
        return str(self._list)
    #--- End: def

    def __contains__(self, item):
        '''
x.__contains__(y) <==> y in x

Uses numerically tolerant equality where appropriate.

'''
        for x in self._list:
            if equals(item, x, traceback=False):
                return True
        #--- End: for

        return False
    #--- End: def
    
    def __len__(self):
        '''
x.__len__() <==> len(x)

'''
        return len(self._list)
    #--- End: def

    def __delitem__(self, index):
        '''
x.__delitem__(index) <==> del x[index]

'''
        del self._list[index]
    #--- End: def

    def __getitem__(self, index):
        '''
x.__getitem__(index) <==> x[index]

'''
        out = self._list[index]
        try:
            int(index)
        except TypeError:
            # index is a slice instance, so return a CfList instance
            out = type(self)(out)

        return out
    #--- End: def

    def __setitem__(self, index, value):        
        '''
x.__setitem__(index, value) <==> x[index]=value

'''
        try:
            self._list[index] = value._list
        except AttributeError:
            self._list[index] = value
    #--- End: def

    def __eq__(self, other):
        '''
x.__eq__(y) <==> x==y <==> x.equals(y)

'''
        return self.equals(other)
    #--- End: def

    def __ne__(self, other):
        '''
x.__ne__(y) <==> x!=y <==> not x.equals(y)

'''
        return not self.__eq__(other)
    #--- End: def

    def __add__(self, other):
        '''
x.__add__(y) <==> x+y

'''
        new = self.copy()
        new.extend(other)
        return new
    #--- End: def

    def __mul__(self, other):
        '''
x.__mul__(n) <==> x*n

'''
        return type(self)(self._list * other)
    #--- End: def

    def __rmul__(self, other):
        '''
x.__rmul__(n) <==> x*n

'''
        return self.__mul__(other)
    #--- End: def

    def __iadd__(self, other):
        '''
x.__iadd__(y) <==> x+=y

'''
        self.extend(other)
        return self
    #--- End: def

    def __imul__(self, other):
        '''
x.__imul__(n) <==> x*=n

'''
        self._list = self._list * other
        return self
    #--- End: def

    def count(self, value):
        '''

Return the number of occurrences of a given value.

Uses numerically tolerant equality where appropriate.

:Parameters:

    value :
        The value to count.

:Returns:

    out : int
        The number of occurrences of `value`.

**Examples**

>>> s
[1, 2, 3, 2, 4, 2]
>>> s.count(1)
1
>>> s.count(2)
3

'''
        return sum(1
                   for x in self._list 
                   if equals(value, x, traceback=False))
    #--- End def

    def index(self, value, start=0, stop=None):
        '''

Return the first index of a given value.

Uses numerically tolerant equality where appropriate.

:Parameters:

    value :
        The value to look for in the list.

    start : int, optional
        Start looking from this index. By default, look from the
        beginning of the list.

    stop : int, optional
        Stop looking before this index. By default, look up to the end
        of the list.

:Returns:

    out : int

:Raises:

    ValueError :
        If the given value is not in the list.

**Examples**

>>> s
[1, 2, 3, 2, 4, 2]
>>> s.index(1)
1
>>> s.index(2, 2)
3
>>> s.index(2, start=2, stop=5)
3
>>> s.index(6)
ValueError: CfList doesn't contain: 6

'''      
        if start < 0:
            start = len(self) + start

        if stop is None:
            stop = len(self)
        elif stop < 0:
            stop = len(self) + stop

        for i, x in enumerate(self[start:stop]):
            if equals(value, x, traceback=False):
               return i + start
        #--- End: for

        raise ValueError("%s doesn't contain: %s" % 
                         (self.__class__.__name__, repr(value)))
    #--- End: def

    def insert(self, index, item):
        '''

Insert an object before the given index in place.

:Parameters:

    index : int

    item :

:Returns:

    None

**Examples**

>>> s
[1, 2, 3]
>>> s.insert(1, 'A')
>>> s
[1, 'A', 2, 3]
>>> s.insert(100, 'B')
[1, 'A', 2, 3, 'B']
>>> s.insert(100, 'B')
[1, 'A', 2, 3, 'B']
>>> s.insert(-2, 'C')
[1, 'A', 2, 'C', 3, 'B']
>>> s.insert(-100, 'D')
['D', 1, 'A', 2, 'C', 3, 'B']

'''
        if hasattr(item, '_list'):
            self._list.insert(index, item._list)
        else:
            self._list.insert(index, item)
    #--- End: def

    def copy(self):
        '''

Return a deep copy.

Equivalent to ``copy.deepcopy(s)``.

:Returns:

    out : 
        The deep copy.

**Examples**

>>> s.copy()

'''
        new = type(self)()

        for attr, value in self.__dict__.iteritems():
            setattr(new, attr, deepcopy(value))

        return new
    #--- End: def

    def equals(self, other, rtol=None, atol=None, traceback=False):
        '''

True if two instances are equal, False otherwise.

Two instances are equal if their attributes are equal and their
elements are equal pair-wise.

:Parameters:

    other : 
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

    traceback : bool, optional
        If True then print a traceback highlighting where the two
        instances differ.

:Returns: 

    out : bool
        Whether or not the two instances are equal.

'''
        if self is other:
            return True
         
        # Check that each instance is the same type
        if self.__class__ != other.__class__:
            if traceback:
                print("%s: Different types: %s, %s" %
                      (self.__class__.__name__,
                       self.__class__.__name__,
                       other.__class__.__name__))
            return False
        #--- End: if

        # Check that the lists have the same number of elements
        if len(self) != len(other): 
            if traceback:
                print("%s: Different attributes: %s, %s" %
                      (self.__class__.__name__,
                       attrs.symmetric_difference(other.__dict__)))
            return False
        #--- End: if

        # Check the attributes
        attrs = set(self.__dict__)
        if attrs != set(other.__dict__):
            if traceback:
                print("%s: Different attributes: %s, %s" %
                      (self.__class__.__name__,
                       attrs.symmetric_difference(other.__dict__)))
            return False
        #--- End: if

        if rtol is None:
            rtol = RTOL()
        if atol is None:
            atol = ATOL()

        for attr, value in attrs - set(('_list',)):
            x = getattr(self, attr)
            y = getattr(other, attr)
            if not equals(x, y, rtol=rtol, atol=atol, traceback=traceback):
                if traceback:
                    print("%s: Different '%s': %s, %s" %
                          (self.__class__.__name__, attr, x, y))
                return False
        #--- End: for

        # Check the element values
        for x, y in zip(self._list, other._list):
            if not equals(x, y, rtol=rtol, atol=atol, traceback=traceback):
                if traceback:
                    print("%s: Different elements: %s, %s" %
                          (self.__class__.__name__, repr(x), repr(y)))
                return False
        #--- End: for

        return True
    #--- End: def
    
#--- End: class


# ====================================================================
#
# CfDict object
#
# ====================================================================

class CfDict(collections.MutableMapping):
    '''

A dictionary-like object with attributes.

'''
    def __init__(self, *args, **kwargs):
        '''

**Initialization**

:Parameters:

    args, kwargs :
        Keys and values are initialized exactly as for a built-in
        dict.

'''

        self._dict = dict(*args, **kwargs)
    #--- End: def

    def __deepcopy__(self, memo):
        '''
Used if copy.deepcopy is called on the variable.

''' 
        return self.copy()
    #--- End: def

    def __repr__(self):
        '''
x.__repr__() <==> repr(x)

'''
        return repr(self._dict)
    #--- End: def

    def __str__(self):
        '''
x.__str__() <==> str(x)

'''
        return str(self._dict)
    #--- End: def

    def __getitem__(self, key):
        '''
x.__getitem__(key) <==> x[key]

'''     
        return self._dict[key]
    #--- End: def

    def __setitem__(self, key, value):
        '''
x.__setitem__(key, value) <==> x[key]=value

'''
        self._dict[key] = value
    #--- End: def

    def __delitem__(self, key):
        '''
x.__delitem__(key) <==> del x[key]

'''
        del self._dict[key]
    #--- End: def

    def __iter__(self):
        '''
x.__iter__() <==> iter(x)

'''
        return iter(self._dict)
    #--- End: def

    def __len__(self):
        '''
x.__len__() <==> len(x)

'''
        return len(self._dict)
    #--- End: def

#    def __contains__(self, item):
#        '''
#Test for set membership using numerically tolerant equality.
#'''
#        for s in self:
#            if equals(item, s):
#                return True
#
#        return False
#    #--- End: def

    def __eq__(self, other):
        '''
x.__eq__(y) <==> x==y <==> x.equals(y)

'''
        return self.equals(other)
    #--- End: def

    def __ne__(self, other):
        '''
x.__ne__(y) <==> x!=y <==> not x.equals(y)

'''
        return not self.__eq__(other)
    #--- End: def

    def copy(self):
        '''

Return a deep copy.

Equivalent to ``copy.deepcopy(d)``.

:Returns:

    out : 
        The deep copy.

**Examples**

>>> d.copy()

'''
        new = type(self)()

        for attr, value in self.__dict__.iteritems():
            setattr(new, attr, deepcopy(value))

        return new
    #--- End: def

    def equals(self, other, rtol=None, atol=None, traceback=False):
        '''

True if two instances are logically equal, False otherwise.

:Parameters:

    other : 
        The object to compare for equality.

    atol : float, optional
        The absolute tolerance for all numerical comparisons, By
        default the value returned by the `ATOL` function is used.

    rtol : float, optional
        The relative tolerance for all numerical comparisons, By
        default the value returned by the `RTOL` function is used.

    traceback : bool, optional
        If True then print a traceback highlighting where the two
        instances differ.

:Returns: 

    out : bool
        Whether or not the two instances are equal.

**Examples**

'''
        if self is other:
            return True
        
        # Check that each instance is the same type
        if self.__class__ != other.__class__:
            if traceback:
                print("%s: Different types: %s, %s" %
                      (self.__class__.__name__,
                       self.__class__.__name__,
                       other.__class__.__name__))
            return False
        #--- End: if

#        # Check the attributes
#        attrs = set(self.__dict__)
#        if attrs != set(other.__dict__):
#            if traceback:
#                print("%s: Different attributes: %s" %
#                      (self.__class__.__name__,
#                       attrs.symmetric_difference(other.__dict__)))
#            return False
#        #--- End: if

        if rtol is None:
            rtol = RTOL()
        if atol is None:
            atol = ATOL()

#        for attr in attrs.difference(('_dict',)):
#            x = getattr(self, attr)
#            y = getattr(other, attr)
#            if not equals(x, y, rtol=rtol, atol=atol, traceback=traceback):
#                if traceback:
#                    print("%s: Different '%s' attributes: %s, %s" %
#                          (self.__class__.__name__, attr, x, y))
#                return False
#        #--- End: for

        # Check that the keys are equal
        if set(self) != set(other):
            if traceback:
                print("%s: Different keys: %s" %
                      (self.__class__.__name__,
                       set(self).symmetric_difference(other)))
            return False
        #--- End: if

        # Check that the key values are equal
        for key, value in self.iteritems():
            if not equals(value, other[key], rtol=rtol, atol=atol,
                          traceback=traceback):
                if traceback:
                    print("%s: Different '%s' values: %s, %s" %
                          (self.__class__.__name__, key,
                           repr(value), repr(other[key])))
                return False
        #--- End: for
                
        # Still here?
        return True
    #--- End: def

    def get_keys(self, regex=None):
        ''' 

Return a list of the key names which match a regular expression.

:Parameters:

    regex : str, optional
        The regular expression with which to identify key names. By
        default all keys names are returned.

:Returns: 

    out : list
        A list of key names.

**Examples**

>>> d.keys()
['dim2', 'dim0', 'dim1', 'aux0', 'cm0']
>>> d.get_keys()
['dim2', 'dim0', 'dim1', 'aux0', 'cm0']
>>> d.get_keys('dim')
['dim2', 'dim0', 'dim1']
>>> d.get_keys('^aux|^dim')
['dim2', 'dim0', 'dim1', 'aux0']
>>> d.get_keys('dim[1-9]')
['dim2', 'dim1']

'''
        if regex is None:
            return self.keys()

        keys = []        
        for key in self:
            if re_search('%s' % regex, key):
                keys.append(key)
        #--- End: for

        return keys
    #--- End: def

    def has_key(self, key):
        '''

Return true if and only if the dictionary contains the given key.

:Parameters:

    key : hashable object
        The key.

:Returns:

    out : bool

**Examples**

>>> d.keys()
['key1', 3, ('a', 1)]
>>> d.has_key(3)
True
>>> d.has_key('key9')
False

'''
        return self._dict.has_key(key)
    #--- End: def

#--- End: class
